/*
 * Copyright 2008 M-Way Solutions
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.mwaysolutions.ocm.impl;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Map;

import javax.jcr.Item;
import javax.jcr.ItemVisitor;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springmodules.jcr.JcrUtils;

import com.mwaysolutions.ocm.OcmException;
import com.mwaysolutions.ocm.OcmPropertyDescriptor;
import com.mwaysolutions.ocm.OcmReader;
import com.mwaysolutions.ocm.OcmTypeRegister;
import com.mwaysolutions.ocm.OcmUtils;



/**
 * OCM Reader Implementation
 *
 * @author <a href="mailto:m.zdila@mwaysolutions.com">Martin Ždila</a>
 */
public class OcmReaderImpl implements OcmReader {
	
	private static final Log log = LogFactory.getLog(OcmReader.class);

	static final String JCR_PREFIX = "jcr:";

	private final OcmTypeRegister ocmTypeRegister;

	private final Map<IsSameItemEqualsWrapper, Object> itemToObjectMap;
	

	public OcmReaderImpl(final OcmTypeRegister ocmTypeRegister, final Map<IsSameItemEqualsWrapper, Object> itemToObjectMap) {
		this.ocmTypeRegister = ocmTypeRegister;
		this.itemToObjectMap = itemToObjectMap;
	}


	@Override
	public Object get(final Item item, final OcmReader ocmReader) throws RepositoryException, OcmException {
		final IsSameItemEqualsWrapper comparableItem = new IsSameItemEqualsWrapper(item);
		if (itemToObjectMap != null && itemToObjectMap.containsKey(comparableItem)) {
			return itemToObjectMap.get(comparableItem);	// return the same object if it has been already instantiated (for referenced nodes)
		}

		class ResultHolder<T> {
			T result;
		}

		final ResultHolder<Object> resultHolder = new ResultHolder<Object>();


		item.accept(new ItemVisitor() {

			@Override
			public void visit(final Property property) throws RepositoryException {
				if (property.getType() == PropertyType.REFERENCE) {
					if (property.getDefinition().isMultiple()) {
						final Value[] values = property.getValues();
						// we can only create array of Objects; this will be later converted in OcmUtils.setProperty
						resultHolder.result = new Object[values.length];
						for (int i = 0; i < values.length; i++) {
							Array.set(resultHolder.result, i, ocmReader.get(property.getSession().getNodeByUUID(values[i].getString()), OcmReaderImpl.this));
						}
					} else {
						resultHolder.result = ocmReader.get(property.getNode(), OcmReaderImpl.this);
					}
				} else {
					resultHolder.result = OcmUtils.getValue(property);
				}

				if (itemToObjectMap != null) {
					itemToObjectMap.put(comparableItem, resultHolder.result);
				}
			}

			@SuppressWarnings("unchecked")
			@Override
			public void visit(final Node node) throws RepositoryException {
				final String nodeTypeName = node.getPrimaryNodeType().getName();

				final Class<?> clazz = ocmTypeRegister.getTypeDescriptorByNodeTypeName(nodeTypeName, true).getClazz();

				if (clazz.isArray()) {
					int n = (int) node.getNodes().getSize();
					for (final PropertyIterator pi = node.getProperties(); pi.hasNext(); ) {
						if (!pi.nextProperty().getName().startsWith(JCR_PREFIX)) {
							n++;
						}
					}

					resultHolder.result = Array.newInstance(clazz.getComponentType(), n);
				} else {
					try {
						resultHolder.result = clazz.newInstance();
					} catch (final InstantiationException e) {
						throw new OcmException(e);
					} catch (final IllegalAccessException e) {
						throw new OcmException(e);
					}
				}

				if (itemToObjectMap != null) {
					itemToObjectMap.put(comparableItem, resultHolder.result);
				}


				if (clazz.isArray()) {
					final Object[] objects = (Object[]) resultHolder.result;
					int index = 0;

					for (final NodeIterator ni = node.getNodes(); ni.hasNext(); ) {
						final Node subNode = ni.nextNode();
						objects[index++] = ocmReader.get(subNode, OcmReaderImpl.this);
					}

					for (final PropertyIterator pi = node.getProperties(); pi.hasNext(); ) {
						final Property property = pi.nextProperty();
						if (!property.getName().startsWith(JCR_PREFIX)) {
							objects[index++] = ocmReader.get(property, OcmReaderImpl.this);
						}
					}
				} else if (resultHolder.result instanceof Collection) {
					final Collection<Object> collection = (Collection) resultHolder.result;

					for (final NodeIterator ni = node.getNodes(); ni.hasNext(); ) {
						final Node subNode = ni.nextNode();
						collection.add(ocmReader.get(subNode, OcmReaderImpl.this));
					}

					for (final PropertyIterator pi = node.getProperties(); pi.hasNext(); ) {
						final Property property = pi.nextProperty();
						if (!property.getName().startsWith(JCR_PREFIX)) {
							collection.add(ocmReader.get(property, OcmReaderImpl.this));
						}
					}
				} else if (resultHolder.result instanceof Map) {
					final Map<Object, Object> map = (Map) resultHolder.result;

					for (final NodeIterator ni = node.getNodes(); ni.hasNext(); ) {
						final Node subNode = ni.nextNode();
						map.put(JcrUtils.decode(subNode.getName()), ocmReader.get(subNode, OcmReaderImpl.this));
					}

					for (final PropertyIterator pi = node.getProperties(); pi.hasNext(); ) {
						final Property property = pi.nextProperty();
						if (!property.getName().startsWith(JCR_PREFIX)) {
							map.put(JcrUtils.decode(property.getName()), ocmReader.get(property, OcmReaderImpl.this));
						}
					}
				} else { // registered class
					for (final NodeIterator ni = node.getNodes(); ni.hasNext(); ) {
						final Node subNode = ni.nextNode();
						final OcmPropertyDescriptor ocmPropertyDescriptor = ocmTypeRegister.getTypeDescriptorByNodeTypeName(nodeTypeName, false).getPropertyDescriptorByJcrPropertyName(subNode.getName());
						if (ocmPropertyDescriptor == null) {
							log.warn("data in JCR does not fit type definition; skipping Node: " + subNode.getPath() + "; nodeType: " + subNode.getPrimaryNodeType().getName());
							continue;
						}
						final String beanPropertyName = ocmPropertyDescriptor.getBeanPropertyName();
						OcmPropertyUtils.setProperty(resultHolder.result, beanPropertyName, ocmReader.get(subNode, OcmReaderImpl.this));
					}

					for (final PropertyIterator pi = node.getProperties(); pi.hasNext(); ) {
						final Property property = pi.nextProperty();
						final String propertyName = property.getName();
						if (!propertyName.startsWith(JCR_PREFIX) || propertyName.equals("jcr:uuid")) {
							final OcmPropertyDescriptor ocmPropertyDescriptor = ocmTypeRegister.getTypeDescriptorByNodeTypeName(nodeTypeName, true).getPropertyDescriptorByJcrPropertyName(propertyName);
							if (ocmPropertyDescriptor == null) {
								log.warn("data in JCR does not fit type definition; skipping Property: " + property.getPath() + "; nodeType: " + node.getPrimaryNodeType().getName());
								continue;
							}
							final String beanPropertyName = ocmPropertyDescriptor.getBeanPropertyName();
//							final Class<?>type;
//							try {
//								type = PropertyUtils.getPropertyType(resultHolder.result, beanPropertyName);
//							} catch (final IllegalAccessException e) {
//								throw new OcmException(e);
//							} catch (final InvocationTargetException e) {
//								throw new OcmException(e);
//							} catch (final NoSuchMethodException e) {
//								throw new OcmException(e);
//							}
							OcmPropertyUtils.setProperty(resultHolder.result, beanPropertyName, ocmReader.get(property, OcmReaderImpl.this /*, deep := type != String.class && type != String[].class*/));
						}
					}
				}
			}

		}); // accept(visitor)

		return resultHolder.result;
	}

}
